文章译自 React useEffect: 4 Tips Every Developer Should Know[1] ,作者为 Helder Esteves。
说是小技巧,其实不小。
我们谈谈 React Hooks 中的useEffects
。 我将与你分享使用useEffect
时应该注意的4个技巧。
useEffect
只应该用于一个目的在 React Hooks 中,你可以使用多次useEffect
函数。这是一个很好的特性,因为,要编写干净的代码,一个函数只服务于一个目的是必要的(就像一句话应该只传达一个想法一样)。
译者注,这在软件开发中,也叫单一职责原则,The single responsibility principle (SRP)。
将useEffects
拆分成短小精炼的单用途函数,也可以防止一些意外的执行(在使用依赖数组时)。
例如,我们假设你有一个与变量varB
无关的varA
,你想建立一个基于useEffect
的递归计数器(借助setTimeout
)。【坏代码】像下面这样:
function App() {
const [varA, setVarA] = useState(0);
const [varB, setVarB] = useState(0);
// 不要这么做!
useEffect(() => {
const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);
return () => {
clearTimeout(timeoutA);
clearTimeout(timeoutB);
};
}, [varA, varB]);
return (
<span>
Var A: {varA}, Var B: {varB}
</span>
);
}
可以看到,变量varA
或varB
任意一个发生变化都会触发两个变量的更新。这就是为什么这个钩子不能正常工作。
由于这是一个很短的例子,你可能会觉得问题很好发现,然而,当函数更长时,伴随更多的代码和变量,再出现问题时就会让你头大了。所以,做正确的事情,按用途来拆分你的useEffect
。
对于上面的情况,应该这样处理:
function App() {
const [varA, setVarA] = useState(0);
const [varB, setVarB] = useState(0);
// 正确的方式
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
useEffect(() => {
const timeout = setTimeout(() => setVarB(varB + 2), 2000);
return () => clearTimeout(timeout);
}, [varB]);
return (
<span>
Var A: {varA}, Var B: {varB}
</span>
);
}
注意:这里的代码只是为了举例说明,目的是帮助你轻松理解useEffect
的问题。通常情况下,当一个变量依赖于它之前的状态时,推荐的方法是用setVarA(varA => varA + 1)
来代替。(感谢 @Michael Landis 的提醒)
我们再来看看上面的例子。如果变量varA
和varB
是完全独立的呢?
在这种情况下,我们可以简单地创建一个自定义钩子来隔离每个变量。这样,你就可以准确地知道每个函数对哪个变量做了什么。
自定义钩子的代码:
function App() {
const [varA, setVarA] = useVarA();
const [varB, setVarB] = useVarB();
return (
<span>
Var A: {varA}, Var B: {varB}
</span>
);
}
function useVarA() {
const [varA, setVarA] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
return [varA, setVarA];
}
function useVarB() {
const [varB, setVarB] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarB(varB + 2), 2000);
return () => clearTimeout(timeout);
}, [varB]);
return [varB, setVarB];
}
现在每个变量都有了自己的钩子。更易于维护和阅读了。
useEffect
继续以setTimeout
为话题,我们给出下面的例子:
function App() {
const [varA, setVarA] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
return <span>Var A: {varA}</span>;
}
出于某种原因,你想计数到5就停止。有正确的实现方法,也有不正确的方法。
我们先来看看不正确的方法:
function App() {
const [varA, setVarA] = useState(0);
// 不要这么做!
useEffect(() => {
let timeout;
if (varA < 5) {
timeout = setTimeout(() => setVarA(varA + 1), 1000);
}
return () => clearTimeout(timeout);
}, [varA]);
return <span>Var A: {varA}</span>;
}
虽然这样做是可行的,但请记住,clearTimeout
将在varA
发生任何变化时都会运行,而setTimeout
则是有条件地运行(小于5时才会运行)。
对于条件化运行useEffect
,推荐的方式是在函数的开头做一个条件返回,像这样:
function App() {
const [varA, setVarA] = useState(0);
useEffect(() => {
if (varA >= 5) return;
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
return <span>Var A: {varA}</span>;
}
在Material UI[2]中我们可以看到这种用法(以及许多其他的框架中),它确保你没有错误地运行useEffect
。
译者注:仔细比较上面2段代码,我们要达到的目的其实就是,要让
setTimeout
和clearTimeout
成对的出现。而上面不对的代码中,可能会出现if
语句把setTimeout
拦截了但clearTimeout
仍然出现的情况。所以第二种代码风格(提前return),是更好的条件化运行useEffect
的方式。
useEffect
中用到的属性都要在依赖数组中声明如果你正在使用ESLint,那么你可能已经看到过一个来自ESLint exhaustive-deps[3]规则的警告。
这一点至关重要。当你的应用程序越来越大时,更多的依赖关系(属性)会被添加到相关的useEffect
中。为了跟踪所有这些依赖,并避免过期的状态(stale closures),你应该将每一个依赖添加到依赖数组中。(这里有官方对这个问题的看法[4])
同样关于setTimeout
的话题,假设你想只运行一次setTimeout
,并给varA
加1,就像前面的例子一样。
可能你尝试的是下面这样的代码:
function App() {
const [varA, setVarA] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, []); // 小心了: varA 没放在依赖数组中!
return <span>Var A: {varA}</span>;
}
虽然这样做也能达到你的目的,但我们还是要花点时间想一想,"如果代码量变大了怎么办?",或者,"如果我想把上面的代码改成其他的东西怎么办?"
在这种情况下,你会希望把所有的依赖变量都加入进来,因为这将更容易测试和检测可能出现的问题(像过期的属性,stale props and closures)。
所以正确的方式是:
function App() {
const [varA, setVarA] = useState(0);
useEffect(() => {
if (varA > 0) return;
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]); // 非常棒,依赖数组得到了正确设置
return <span>Var A: {varA}</span>;
}
就这样了,各位。如果你有什么问题或建议,我会认真听。 在下面回复或评论吧!
译者注:
之前在某博客上看到过一个有关
useEffect
用法的示例图,非常简洁明了,这里分享给大家:
React useEffect: 4 Tips Every Developer Should Know: https://medium.com/swlh/useeffect-4-tips-every-developer-should-know-54b188b14d9c
[2]Material UI: https://material-ui.com/
[3]exhaustive-deps rule: https://www.npmjs.com/package/eslint-plugin-react-hooks
[4]Can I skip an effect on updates?: https://reactjs.org/docs/hooks-faq.html#can-i-skip-an-effect-on-updates